package campus.aiit.ssl.manage.mybatis;

import campus.aiit.ssl.dao.UserDao;
import com.iflytek.uaac.client.rest.context.UaacServiceContext;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.scripting.xmltags.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.lang.reflect.Field;
import java.lang.reflect.Parameter;
import java.util.List;

/**
 * @author ihaokun
 * @date 2020/1/8 14:35
 */
//@Component
@Intercepts({
    @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
//    ,@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})
})
@Slf4j
public class ExecutorInterceptor implements Interceptor {
  final int MAPPED_STATEMENT_INDEX = 0;
  final int PARAM_MAP_INDEX = 1;  //mapper method params

  @Autowired private ApplicationContext context;

  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    String userId = getUserId();
    Object[] args = invocation.getArgs();
    if (userId != null){
      modifySqlIfIncludeUser(((MappedStatement) args[MAPPED_STATEMENT_INDEX]).getSqlSource(), userId);
    }
    return invocation.proceed();
  }

  private String getUserId(){
    ServletRequestAttributes attributes;
    // whether is a request
    try {
      attributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
    } catch (IllegalStateException e) {
      // e.printStackTrace();
      log.info("Non-Request(Application Internal Task) Execute the query SQL");
      return null;
    }
    return UaacServiceContext.getUaacService().getCurrentLoginName(attributes.getRequest().getSession(true));// Context get Session for UAAC get userId
  }
  /**
   * 根据原始SQL判断是否含有userId的if动态SQL<br/>
   * 实际是解析XML Tree的过程<br/>
   * 因为含有大量private final修饰的Field，所以这个方法中大量运用了反射(Reflection)
   *
   * @param sqlSource 原始SQL对象
   * @return 是否包含userId
   * @throws Throwable
   */
  private void modifySqlIfIncludeUser(SqlSource sqlSource, String userId) throws Throwable {
    if (sqlSource instanceof DynamicSqlSource){
      Field rootSqlNodeField = sqlSource.getClass().getDeclaredField("rootSqlNode");
      rootSqlNodeField.setAccessible(true);
      SqlNode rootSqlNode = (SqlNode) rootSqlNodeField.get(sqlSource);
      Field contentsField = rootSqlNode.getClass().getDeclaredField("contents");
      contentsField.setAccessible(true);
      List<SqlNode> contents = (List<SqlNode>) contentsField.get(rootSqlNode);
      for (int i = 0; i < contents.size(); i++) {
        if (contents.get(i) instanceof IfSqlNode){
          Field testField = contents.get(i).getClass().getDeclaredField("test");
          testField.setAccessible(true);
          String test = (String) testField.get(contents.get(i));
          if (test.contains("userId")){ //CRUX 修改if标签，或删除它（因为无法注入参数；无法根据参数判断）
            if (context.getBean(UserDao.class).selectByPrimaryKey(userId).getRoleId() != 0){
              Field subContentsField = contents.get(i).getClass().getDeclaredField("contents");
              subContentsField.setAccessible(true);
              MixedSqlNode subContents = (MixedSqlNode) subContentsField.get(contents.get(i));
              Field subSubcontentsField = subContents.getClass().getDeclaredField("contents");
              subSubcontentsField.setAccessible(true);
              List<SqlNode> subSubContents = (List<SqlNode>) subSubcontentsField.get(subContents);
              StaticTextSqlNode staticTextSqlNode = (StaticTextSqlNode) subSubContents.get(0);
              Field textField = staticTextSqlNode.getClass().getDeclaredField("text");
              textField.setAccessible(true);
              String text = (String) textField.get(staticTextSqlNode);
              contents.set(i, new StaticTextSqlNode(text.replace("?", "'" + userId + "'")));
            } else {
              contents.remove(i);
            }
            return; //FIXME 也许之后会出现多个关联userId的if标签，如insert方法；后续再修改，暂时只对query方法进行拦截
          }
        }
      }
    }
  }

  private void modify4(Object[] args) {
    ServletRequestAttributes attributes;
    try {
      // whether is a request
      attributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
    } catch (IllegalStateException e) {
      // e.printStackTrace();
      log.info("Non-Request(Application Internal Task) Execute the query SQL");
      return;
    }
    String userId = UaacServiceContext.getUaacService().getCurrentLoginName(attributes.getRequest().getSession(true));// Context get Session for UAAC get userId
    if (userId != null) {
      MappedStatement ms = (MappedStatement)args[MAPPED_STATEMENT_INDEX];
      BoundSql boundSql = ms.getBoundSql(args[PARAM_MAP_INDEX]);
      // judge
      String rawSql = boundSql.getSql();
      if ((rawSql.substring(rawSql.indexOf("from")).contains(" device ") || rawSql.substring(rawSql.indexOf("from")).contains(" strategy "))
          && context.getBean(UserDao.class).selectByPrimaryKey(userId).getRoleId() != 0) {
        String append = " and user_id = '" + userId + "'";
        String newSql = rawSql + append;
        BoundSql newBoundSql = new BoundSql(ms.getConfiguration(), newSql, boundSql.getParameterMappings(), boundSql.getParameterObject());
        MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), ms.getId(), new SqlSource() {
          @Override
          public BoundSql getBoundSql(Object o) {
            return newBoundSql;
          }
        }, ms.getSqlCommandType());
        copyFromMs(ms, builder);
        MappedStatement newMs = builder.build();
        args[MAPPED_STATEMENT_INDEX] = newMs;
      }
    }
  }

  private void copyFromMs(MappedStatement ms, MappedStatement.Builder builder){
    builder.resource(ms.getResource());
    builder.databaseId(ms.getDatabaseId());
    builder.fetchSize(ms.getFetchSize());
    builder.statementType(ms.getStatementType());
    builder.keyGenerator(ms.getKeyGenerator());
    // setStatementTimeout()
    builder.timeout(ms.getTimeout());
    // setParameterMap()
    builder.parameterMap(ms.getParameterMap());
    // setStatementResultMap()
    builder.resultMaps(ms.getResultMaps());
    builder.resultSetType(ms.getResultSetType());
    builder.resultOrdered(ms.isResultOrdered());
    // setStatementCache()
    builder.cache(ms.getCache());
    builder.flushCacheRequired(ms.isFlushCacheRequired());
    builder.useCache(ms.isUseCache());
  }
}